﻿using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;

namespace Devonline.AspNetCore;

/// <summary>
/// 业务数据及其附件操作类服务
/// 业务数据的附件全都在名为 Attachments 的集合中, 根据 BusinessType 字段区分用途
/// </summary>
/// <typeparam name="TDbContext">数据库上下文</typeparam>
/// <typeparam name="TEntitySet">业务数据类型</typeparam>
/// <typeparam name="TAttachment">附件数据类型</typeparam>
/// <typeparam name="TKey">主键类型</typeparam>
public class DataWithAttachmentService<TDbContext, TEntitySet, TAttachment, TKey> :
    DataService<TDbContext, TEntitySet, TKey>,
    IDataWithAttachmentService<TDbContext, TEntitySet, TAttachment, TKey>,
    IDataService<TDbContext, TEntitySet, TKey>,
    IDataAttachmentService<TDbContext, TEntitySet, TAttachment, TKey>
    where TDbContext : DbContext
    where TEntitySet : class, IEntitySet<TKey>, new()
    where TAttachment : class, IAttachment<TKey>
    where TKey : IConvertible
{
    protected readonly IFileService _fileService;
    protected readonly Type _attachmentType;
    protected readonly DbSet<TAttachment> _dbSetAttachment;
    public DataWithAttachmentService(
        ILogger<DataWithAttachmentService<TDbContext, TEntitySet, TAttachment, TKey>> logger,
        TDbContext context,
        IDistributedCache cache,
        IHttpContextAccessor httpContextAccessor,
        IFileService fileService,
        HttpSetting httpSetting) : base(logger, context, cache, httpContextAccessor, httpSetting)
    {
        _fileService = fileService;
        _attachmentType = typeof(TAttachment);
        _dbSetAttachment = _context.Set<TAttachment>();
    }

    #region 实现自 IDataAttachmentService
    /// <summary>
    /// 新增附件
    /// </summary>
    /// <param name="entitySet">业务数据</param>
    /// <param name="attachments">附件集合</param>
    /// <param name="businessType">业务类型</param>
    /// <returns>待删除文件列表, 请在数据库事物提交成功后删除列表中的文件</returns>
    public virtual async Task AddAttachmentsAsync(TEntitySet entitySet, IEnumerable<TAttachment>? attachments = default, string? businessType = default)
    {
        attachments ??= entitySet.GetCollectionMember<TEntitySet, TAttachment>();
        businessType ??= _type.Name;
        if (attachments is not null && attachments.Any())
        {
            var propertyInfos = _propertyInfos.Where(x => Type.GetTypeCode(x.PropertyType) == TypeCode.String && x.HasAttribute<BusinessTypeAttribute>());
            foreach (var attachment in attachments)
            {
                attachment.BusinessKey = entitySet.Id;
                attachment.BusinessType ??= businessType;
                Create(attachment);
                await _dbSetAttachment.AddAsync(attachment);

                //设置附件对应的字段值
                propertyInfos.FirstOrDefault(x => x.GetBusinessType() == attachment.BusinessType)?.SetValue(entitySet, attachment.Path);
            }
        }
    }
    /// <summary>
    /// 修改附件, 保留旧附件, 新增新附件, 返回待删除文件列表
    /// 附件的查询是按 BusinessKey 的值进行, 因此不同类型的 Id 不能为同一个值
    /// businessType 作为业务类型, 只要传递了值, 则调用此方法后, 将只处理这一种 BusinessType 类型的附件
    /// </summary>
    /// <param name="entitySet">业务数据</param>
    /// <param name="attachments">附件集合</param>
    /// <param name="businessType">业务类型</param>
    /// <param name="isLogical">是否逻辑操作, 默认不是</param>
    /// <returns>待删除文件列表, 请在数据库事物提交成功后删除列表中的文件</returns>
    public virtual async Task<IEnumerable<string>?> UpdateAttachmentsAsync(TEntitySet entitySet, IEnumerable<TAttachment>? attachments = default, string? businessType = default, bool isLogical = false)
    {
        var news = attachments ?? entitySet.GetCollectionMember<TEntitySet, TAttachment>() ?? new List<TAttachment>();
        var queryable = _dbSetAttachment.Where(x => x.BusinessKey != null && x.BusinessKey.Equals(entitySet.Id));
        if (businessType is not null)
        {
            queryable = queryable.Where(x => x.BusinessType == businessType);
        }

        var olds = await queryable.ToListAsync() ?? [];
        if (!news.Any() && olds.Count == 0)
        {
            return null;
        }

        businessType ??= _type.Name;
        var propertyInfos = _propertyInfos.Where(x => Type.GetTypeCode(x.PropertyType) == TypeCode.String && x.HasAttribute<BusinessTypeAttribute>());
        foreach (var attachment in news)
        {
            if (!olds.Any(x => x.Path == attachment.Path || x.Id.Equals(attachment.Id)))
            {
                //新的有旧的没有, 则新增, 附件只有新增和删除, 没有修改的说法
                attachment.BusinessKey = entitySet.Id;
                attachment.BusinessType ??= businessType;
                Create(attachment);
                _dbSetAttachment.Add(attachment);

                //设置附件对应的字段值
                propertyInfos.FirstOrDefault(x => x.GetBusinessType() == attachment.BusinessType)?.SetValue(entitySet, attachment.Path);
            }
        }

        var files = new List<string>();
        foreach (var attachment in olds)
        {
            //旧的有新的没有则删除
            //找 Id 或者 Path 值相等的, 新的里面找不到则删除旧的
            if ((!string.IsNullOrWhiteSpace(attachment.Path)) && (!news.Any(x => x.Id.Equals(attachment.Id) || x.Path == attachment.Path)))
            {
                files.Add(attachment.Path);
                Delete(attachment, isLogical);
            }
        }

        return files;
    }
    /// <summary>
    /// 自动删除附件, 返回待删除文件列表, 适用于实体对象模型从数据库删除时查找原始附件并自动删除
    /// </summary>
    /// <param name="id">业务数据主键</param>
    /// <param name="businessType">业务类型</param>
    /// <param name="isLogical">是否逻辑操作, 默认不是</param>
    /// <returns>待删除文件列表, 请在数据库事物提交成功后删除列表中的文件</returns>
    public virtual async Task<IEnumerable<string>?> DeleteAttachmentsAsync(TKey id, string? businessType = default, bool isLogical = false)
    {
        var queryable = _dbSetAttachment.Where(x => x.BusinessKey != null && x.BusinessKey.Equals(id));
        if (!string.IsNullOrWhiteSpace(businessType))
        {
            queryable = queryable.Where(x => x.BusinessType == businessType);
        }

        var attachments = await queryable.ToListAsync();
        if (attachments.Count < 1)
        {
            return null;
        }

        var files = attachments.Select(x => x.Path ?? string.Empty).Distinct();
        foreach (var attachment in attachments)
        {
            Delete(attachment, isLogical);
        }

        return files;
    }
    /// <summary>
    /// 按文件列表删除附件
    /// </summary>
    /// <param name="fileNames">待删除的文件名</param>
    /// <param name="isLogical">是否逻辑操作, 默认不是</param>
    /// <returns></returns>
    public virtual async Task<IEnumerable<string>?> DeleteAttachmentsAsync(IEnumerable<string> fileNames, bool isLogical = false)
    {
        var attachments = await _dbSetAttachment.Where(x => x.Path != null && fileNames.Contains(x.Path)).ToListAsync();
        if (attachments.Count < 1)
        {
            return null;
        }

        var files = attachments.Select(x => x.Path ?? string.Empty).Distinct();
        foreach (var attachment in attachments)
        {
            Delete(attachment, isLogical);
        }

        return files;
    }
    #endregion

    #region 重载自 DataService 内部方法
    /// <summary>
    /// 重载的新增方法
    /// 默认的附件本身仅仅存在于 Attachments 集合中, 此处仅处理默认的一种情况
    /// </summary>
    /// <param name="entitySet">业务数据</param>
    /// <param name="context">数据操作上下文</param>
    /// <returns></returns>
    protected override async Task InternalAddAsync(TEntitySet entitySet, DataServiceContext? context = null)
    {
        _logger.LogInformation($"{_userAccess}: will add {_typeName} and it's attachments, the content is: {entitySet.ToJsonString()}");

        await base.InternalAddAsync(entitySet, context);

        //此方法要求新增附件时, 每一个附件都已设置好 BusinessType
        await AddAttachmentsAsync(entitySet, entitySet.GetCollectionMember<TEntitySet, TAttachment>(), default);
    }
    /// <summary>
    /// 重载的更新方法
    /// 默认的附件本身仅仅存在于 Attachments 集合中, 此处仅处理默认的一种情况
    /// </summary>
    /// <param name="entitySet">业务数据</param>
    /// <param name="context">数据操作上下文</param>
    /// <returns></returns>
    protected override async Task InternalUpdateAsync(TEntitySet entitySet, DataServiceContext? context = null)
    {
        var isLogical = context?.IsLogical ?? false;
        _logger.LogInformation($"{_userAccess}: will {GetLogicalString(isLogical)} update {_typeName} and it's attachments, the content is: {entitySet.ToJsonString()}");

        await base.InternalUpdateAsync(entitySet, context);

        var files = await UpdateAttachmentsAsync(entitySet, entitySet.GetCollectionMember<TEntitySet, TAttachment>(), default, isLogical);
        if (files is not null && files.Any())
        {
            context ??= new DataServiceContext();
            context.After += () => _fileService.Delete(files.ToArray());
        }
    }
    /// <summary>
    /// 重载的删除方法会同时将当前业务数据的附加信息一并删除
    /// </summary>
    /// <param name="id">业务数据主键</param>
    /// <param name="context">数据操作上下文</param>
    /// <returns></returns>
    protected override async Task InternalDeleteAsync(TKey id, DataServiceContext? context = null)
    {
        var isLogical = context?.IsLogical ?? false;
        _logger.LogInformation($"{_userAccess}: will {GetLogicalString(isLogical)} delete {_typeName} and it's attachments, the content id is: {id}");

        await base.InternalDeleteAsync(id, context);
        var files = await DeleteAttachmentsAsync(id, default, isLogical);
        if (files is not null && files.Any())
        {
            context ??= new DataServiceContext();
            context.After += () => _fileService.Delete(files.ToArray());
        }
    }
    #endregion
}

/// <summary>
/// 业务数据及其附件操作类服务
/// 字符串作为主键的默认实现
/// 业务数据的附件全都在名为 Attachments 的集合中, 根据 BusinessType 字段区分用途
/// </summary>
/// <typeparam name="TDbContext">数据库上下文</typeparam>
/// <typeparam name="TEntitySet">业务数据类型</typeparam>
public class DataWithAttachmentService<TDbContext, TEntitySet>(
    ILogger<DataWithAttachmentService<TDbContext, TEntitySet>> logger,
    TDbContext context,
    IDistributedCache cache,
    IHttpContextAccessor httpContextAccessor,
    IFileService fileService,
    HttpSetting httpSetting) :
    DataWithAttachmentService<TDbContext, TEntitySet, Attachment, string>(logger, context, cache, httpContextAccessor, fileService, httpSetting),
    IDataWithAttachmentService<TDbContext, TEntitySet>,
    IDataService<TDbContext, TEntitySet>,
    IDataAttachmentService<TDbContext, TEntitySet>
    where TDbContext : DbContext
    where TEntitySet : class, IEntitySet, new();